﻿#region Copyright Statement
//////////////////////////////////////////////
//      Created By:   Ali Ghomi             //
//      Date:         16 NOV 2009           //
//////////////////////////////////////////////
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Data;
using ORMAC.Attributes;
using System.Diagnostics;
using ORMAC.Reflection;

namespace ORMAC.Core
{
    /// <summary>
    /// Extension & Helper methods for Schema data conversion
    /// No Schema specific logic should be placed here
    /// </summary>
    internal static class ReflectionHelper
    {
        /// <summary>
        /// converts a string to enumeration based on specified schema attribs
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="figureName">enum attribute text</param>
        /// <param name="schema">schema</param>
        /// <returns></returns>
        public static T ToEnum<T>(this string figureName,string schema=null)
        {
            string lfigureName =figureName!=null? figureName.ToLower():null;
            if(string.IsNullOrEmpty(lfigureName))
                return (T)Enum.Parse(typeof(T), "Unknown");
            var allFields = typeof(T).GetFields().ToList<FieldInfo>();
            foreach (var info in allFields)
            {
                var attribs = info.GetCustomAttributes(true);
                foreach (var attrib in attribs)
                {
                    CodeAttribute cAttrib = attrib as CodeAttribute;
                    if (cAttrib!=null)
                    {
                        if (schema !=null && cAttrib.Schema != schema)
                            continue;
                        string code = cAttrib.Code != null ? cAttrib.Code.ToLower() : null;
                        if ((!cAttrib.IsExact && lfigureName.LastIndexOf(code) >= 0)||
                        (lfigureName==code))
                        {
                            if (!cAttrib.IsNumber)
                                return (T)Enum.Parse(typeof(T), info.Name);
                            else//check for the whole numbers
                            {
                                int index = lfigureName.LastIndexOf(code) + code.Length;
                                if(index>=lfigureName.Length || !char.IsDigit(lfigureName[index]))
                                    return (T)Enum.Parse(typeof(T), info.Name);
                            }
                        }
                    }
                }
            }
                return (T)Enum.Parse(typeof(T), "Unknown");
        }
        public static string ToCode(this Enum eValue)
        {
            var allFields = eValue.GetType().GetFields().ToList<FieldInfo>();
            if (allFields == null)
                return null;
            var info = allFields.Find(a => a.Name == eValue.ToString());
            var allAttribs = info.GetCustomAttributes(true);
            if (allAttribs == null)
                return null;
            var attrib = allAttribs.ToList().Find(a => a is CodeAttribute);
            if (attrib ==null)
                return null;
            CodeAttribute cAttrib = attrib as CodeAttribute;
            return cAttrib.Code;
        }
        /// <summary>
        /// Gets schema table name of a class(type)
        /// </summary>
        /// <typeparam name="T">type</typeparam>
        /// <param name="schema">schema</param>
        /// <returns></returns>
        
        public static string TableName(Type classType,string schema)
        {
            var attrib=ReflectionCacheManager.GetCustomAttribute<TableAttribute>(schema,classType);
            if (attrib != null)
                return attrib.TableName;
            var attrib1 = ReflectionCacheManager.GetCustomAttributes<SQLAttribute>(schema,classType);
            if (attrib1 != null)
                return classType.Name;
            return null;
        }
        public static string Query(Type classType, string schema)
        {
            //var attribs = classType.GetCustomAttributes(typeof(SQLAttribute),false);
            var attrib = ReflectionCacheManager.GetCustomAttribute<SQLAttribute>(schema,classType);
            if (attrib != null)
                return attrib.Query;
            return null;
        }

        public static string LinkTableName(Type classType, string schema)
        {
            //var attribs = classType.GetCustomAttributes(typeof(LinkTableAttribute),false);
            var attrib = ReflectionCacheManager.GetCustomAttribute<LinkTableAttribute>(schema,classType);
            if (attrib != null)
                return attrib.LinkTableName;
            return null;
        }
        public static string TableName<T>(string schema)
        {
            return TableName(typeof(T), schema);
        }
        public static string Query<T>(string schema)
        {
            return Query(typeof(T), schema);
        }
        /// <summary>
        /// Gets the criteria for selecting a table as specified in the attributes
        /// </summary>
        /// <typeparam name="T">class type</typeparam>
        /// <param name="schema">schema</param>
        /// <returns></returns>
        public static string TableCriteria<T>(string schema)
        {
            var attribs = typeof(T).GetCustomAttributes(typeof(TableAttribute),false);
            if (attribs == null)
                return null;
            foreach (var attrib in attribs)
            {
                TableAttribute tAttrib = attrib as TableAttribute;
                if (tAttrib != null && tAttrib.Schema == schema)
                {

                    return tAttrib.Criteria;
                }
            }
            return null;
        }
        /// <summary>
        /// gets the prefix specified in a property
        /// </summary>
        /// <typeparam name="T">class type</typeparam>
        /// <typeparam name="S">property</typeparam>
        /// <param name="schema">schema</param>
        /// <param name="expression">linq expression</param>
        /// <returns></returns>
        public static string Prefix<T, S>(string schema, Expression<Func<S>> expression)
        {
            string propertyName = GetPropertyName<S>(expression);
            return Prefix<T>(schema, propertyName);
        }
        /// <summary>
        /// gets the prefix specified in a property
        /// </summary>
        /// <typeparam name="T">class type</typeparam>
        /// <param name="schema">schema</param>
        /// <param name="propertyName">name of the property</param>
        /// <returns></returns>
        public static string Prefix<T>(string schema, string propertyName)
        {
            //var attribs = typeof(T).GetProperty(propertyName).GetCustomAttributes(typeof(PrefixAttribute),false);
            var attrib = ReflectionCacheManager.GetCustomAttribute<PrefixAttribute>(schema,typeof(T), propertyName);
            if (attrib != null)
                return attrib.Code;
            return null;
        }
        /// <summary>
        /// gets the table name specified for a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="S"></typeparam>
        /// <param name="schema"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string TableName<T, S>(string schema, Expression<Func<S>> expression)
        {
            string propertyName = GetPropertyName<S>(expression);
            return ReflectionHelper.TableName<T>(schema, propertyName);
        }
        /// <summary>
        /// gets the table name of a property
        /// </summary>
        /// <typeparam name="T">class type</typeparam>
        /// <param name="schema">schema name</param>
        /// <param name="propertyName">property name</param>
        /// <returns></returns>
        public static string TableName(Type classType,string schema, string propertyName)
        {
            var attrib = ReflectionCacheManager.GetCustomAttribute<TableAttribute>(schema,classType, propertyName);
            if (attrib != null)
                return attrib.TableName;
            //if the table name is not found return class table name
            return null;
        }

        public static string LinkTableName(Type classType, string schema, string propertyName)
        {
            var attrib = ReflectionCacheManager.GetCustomAttribute<LinkTableAttribute>(schema,classType, propertyName);
            if (attrib != null)
                return attrib.LinkTableName;
            //if the table name is not found return class table name
            return null;
        }
        
        public static string TableName<T>(string schema, string propertyName)
        {
            return TableName(typeof(T), schema, propertyName);
        }
        /// <summary>
        /// Checks if prefix should be used for this property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schema"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public static bool UseTablePrefix<T>(string schema, string propertyName)
        {
            object[] attribs;
            if (typeof(T).IsEnum)
            {
                attribs = typeof(T).GetField(propertyName).GetCustomAttributes(typeof(TableAttribute), false);
            }
            else
            {
                attribs = typeof(T).GetProperty(propertyName).GetCustomAttributes(typeof(TableAttribute), false);
            }
            if (attribs != null)
            {
                foreach (var attrib in attribs)
                {
                    TableAttribute fAttrib = attrib as TableAttribute;
                    if (fAttrib != null && fAttrib.Schema == schema)
                    {
                        return fAttrib.UsePrefix;
                    }
                }
            }
            //if the table name is not found return class table name
            return false;
        }
        /// <summary>
        /// Checks if prefix should be used for this property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schema"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
       public static bool UseFieldPrefix<T>(string schema, string propertyName)
        {
            var attribs = typeof(T).GetProperty(propertyName).GetCustomAttributes(typeof(FieldAttribute), false);
            if (attribs == null)
                return false;
            foreach (var attrib in attribs)
            {
                FieldAttribute fAttrib = attrib as FieldAttribute;
                if (fAttrib != null && fAttrib.Schema == schema)
                {
                    return fAttrib.UsePrefix;
                }
            }
            //if the table name is not found return class table name
            return false;
        }
        /// <summary>
        /// Gets the join attribute information
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schema"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
       public static JoinColumns GetJoinInfo(Type classType,string schema, string propertyName)
       {
           //var attribs = classType.GetProperty(propertyName).GetCustomAttributes(typeof(JoinAttribute), false);
           var attrib =ReflectionCacheManager.GetCustomAttribute<JoinAttribute>(schema,classType,propertyName);
           if (attrib!= null)
               return new JoinColumns
               {
                   ChildCol = attrib.ChildCol,
                   ParnetCol = attrib.ParentCol
               };
           //if the table name is not found return class table name
           return null;
       }
        public static JoinColumns  GetJoinInfo<T>(string schema, string propertyName)
        {
            return GetJoinInfo(typeof(T), schema, propertyName);
        }
        public static LinkTables GetLinkInfo<T>(string schema, string propertyName)
        {
            var attrib = ReflectionCacheManager.GetCustomAttribute<LinkTableAttribute>(schema,typeof(T), propertyName);
            if (attrib != null)
                return new LinkTables
                {
                    ChildCol = attrib.ChildCol,
                    ParnetCol = attrib.ParentCol,
                    LinkTableChildCol = attrib.LinkTableChildCol,
                    LinkTableParnetCol = attrib.LinkTableParentCol,
                    LinkTableName = attrib.LinkTableName
                };
            //if the table name is not found return class table name
            return null;
        }
        /// <summary>
        /// Returns the primary key field name
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schema"></param>
        /// <returns></returns>
        public static IEnumerable<string> GetPrimaryKeys<T>(string tableName, string schema)
        {
            return GetPrimaryKeys(typeof(T),tableName,schema);
        }
        public static IEnumerable<string> GetPrimaryKeys(Type classType,string tableName ,string schema)
        {
            return ReflectionCacheManager.GetPrimaryKeys(classType,schema);
        }
        /// <summary>
        /// Gets the intermediate table name for a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="S"></typeparam>
        /// <param name="schema"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string IntermediateTableName<T, S>(string schema, Expression<Func<S>> expression)
        {
            return IntermediateTableName<T>(schema,GetPropertyName<S>(expression));
        }
        public static string IntermediateTableName<T>(string schema, string propertyName)
        {
            var attribs = typeof(T).GetProperty(propertyName).GetCustomAttributes(typeof(TableAttribute), false);
            if (attribs == null)
                return null;
            foreach (var attrib in attribs)
            {
                TableAttribute fAttrib = attrib as TableAttribute;
                if (fAttrib != null && fAttrib.Schema == schema)
                {
                    return fAttrib.IntermediateTable1;
                }
            }
            return null;
        }
        /// <summary>
        /// Gets the schema field name of a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="S"></typeparam>
        /// <param name="schema"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string FieldName<T,S>(string schema, Expression<Func<S>> expression)
        {
            string propertyName = GetPropertyName<S>(expression);
            return ReflectionHelper.FieldName<T>(schema, propertyName);
        }
        /// <summary>
        /// Gets the schema field name of a property
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schema"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public static string FieldName(Type classType, string schema, string propertyName)
        {
            //var attribs = classType.GetProperty(propertyName).GetCustomAttributes(typeof(FieldAttribute), false);
            var attrib = ReflectionCacheManager.GetCustomAttribute<FieldAttribute>(schema,classType, propertyName);
            if (attrib != null)
                return attrib.FieldName;
            return null;
        }
        public static string DefaultValue(Type classType, string schema, string propertyName)
        {
            var attribs = classType.GetProperty(propertyName).GetCustomAttributes(typeof(FieldAttribute),false);
            foreach (var attrib in attribs)
            {
                FieldAttribute fAttrib = attrib as FieldAttribute;
                if (fAttrib != null && (fAttrib.Schema == schema || string.IsNullOrEmpty(schema)))
                {
                    return fAttrib.Default;
                }
            }
            return null;
        }
        public static string FieldName<T>(string schema, string propertyName)
        {
            return FieldName(typeof(T), schema, propertyName);
        }
        /// <summary>
        /// Helper method to return the name of a property using a linq expression
        /// </summary>
        /// <typeparam name="S"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string GetPropertyName<S>(Expression<Func<S>> expression)
        {
            if (expression.Body is ConstantExpression)
            {
                ConstantExpression body = (ConstantExpression)expression.Body;
                return body.Value.ToString();
            }
            else
            {
                MemberExpression body = (MemberExpression)expression.Body;
                return body.Member.Name;
            }
        }
        /// <summary>
        /// Finds all the tables in the class hierarchy
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="schemaName"></param>
        /// <returns></returns>
        internal static IEnumerable<TableInfo> FindAllTables<T>(string schemaName,ContextMemberCollection contextMembers=null)
        {
            List<TableInfo> allTables = new List<TableInfo>();
            FindAllTablesInternal<T>(schemaName, allTables,new List<Type>(),null,null,contextMembers);
            foreach (TableInfo table in allTables)
            {
                table.Columns = GetTableColumns(table,schemaName);
            }
            return allTables.Distinct();
        }
        /// <summary>
        /// Gets a list of table columns based on property attributes
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static List<ColumnInfo> GetTableColumns(TableInfo table,string schema)
        {
            List<ColumnInfo> res = new List<ColumnInfo>();
            //add the FK column to the parent table
            if (table.JoinInfo != null)
            {
                res.Add(new ColumnInfo
                {
                    DBName = table.JoinInfo.ChildCol,
                    DatasetName = table.Name + "_" + table.JoinInfo.ChildCol,
                });
            }
            foreach (var pInfo in ReflectionCacheManager.GetProperties(table.ClassType))
            {
                string colName=ReflectionHelper.FieldName(table.ClassType, schema, pInfo.Name);
                if (!string.IsNullOrEmpty(colName))
                {
                    ColumnInfo col = new ColumnInfo
                    {
                        DBName = colName,
                        DatasetName = table.Name + "_" + colName,
                        PropertyName=pInfo.Name
                    };
                    if(res.Find(c=>c.DBName==col.DBName)==null)
                        res.Add(col);
                }
                JoinColumns join = ReflectionHelper.GetJoinInfo(table.ClassType, schema, pInfo.Name);
                if (join!=null)
                {
                    ColumnInfo col = new ColumnInfo
                    {
                        DBName = join.ParnetCol,
                        DatasetName = table.Name + "_" + join.ParnetCol,
                        PropertyName = pInfo.Name
                    };
                    if (res.Find(c => c.DBName == col.DBName) == null)
                        res.Add(col);
                }
                /*LinkTables link = ReflectionHelper.GetJoinInfo(table.ClassType, schema, pInfo.Name);
                if (join != null)
                {
                    ColumnInfo col = new ColumnInfo
                    {
                        DBName = join.ParnetCol,
                        DatasetName = table.Name + "_" + join.ParnetCol,
                        PropertyName = pInfo.Name
                    };
                    res.Add(col);
                }*/

            
            }
            return res;
        }
        internal static void FindAllTablesInternal<T>(string schemaName,List<TableInfo> res,List<Type> visitedTypes,JoinColumns join,LinkTables link,ContextMemberCollection context)
        {
            //return if this one has been visited before
            if (visitedTypes.Find(v => v.Name == typeof(T).Name) != null)
                return;
            //add this class to the visited ones
            visitedTypes.Add(typeof(T));
            //Add class custom query tables
            string query = Query(typeof(T), schemaName);
            if (query != null)
            {
                res.Add(new TableInfo { Name = typeof(T).Name, Query = query, ClassType = typeof(T), JoinInfo = join, LinkInfo = link });
            }
            else
            {
                //Add class tables
                var table1 = TableName(typeof(T), schemaName);
                if (table1 != null)
                    res.Add(new TableInfo { Name = table1, ClassType = typeof(T), JoinInfo = join, LinkInfo = link });
            }
            foreach (var pInfo in ReflectionCacheManager.GetProperties(typeof(T)))
            {
                //skip this property if it is not mapped
                if (!ReflectionHelper.IsMappedAndInContext<T>(pInfo, schemaName, context) || !IsAutoLoad(pInfo, schemaName))
                    continue;
                JoinColumns pJoin = GetJoinInfo<T>(schemaName, pInfo.Name);
                if(pJoin!=null)
                    pJoin.ParnetTable = TableName(typeof(T), schemaName);
                LinkTables pLink = GetLinkInfo<T>(schemaName, pInfo.Name);
                if (pLink != null)
                    pLink.ParnetTableName = TableName(typeof(T), schemaName);
                //Handle list properties
                if (pInfo.PropertyType.IsGenericType){
                    foreach (var arg in pInfo.PropertyType.GetGenericArguments())
                    {
                        MethodInfo methodInfo = typeof(ReflectionHelper).GetMethod("FindAllTablesInternal", BindingFlags.Static | BindingFlags.NonPublic);
                        MethodInfo findTablesmethod = methodInfo.MakeGenericMethod(new Type[] { arg });
                        findTablesmethod.Invoke(null, new object[]{schemaName,res,visitedTypes,pJoin,pLink,context});
                    }
                }
                //Handle object properties
                else if (pInfo.PropertyType.IsClass && !pInfo.PropertyType.IsValueType && pInfo.PropertyType != typeof(String) && pInfo.PropertyType != typeof(string))
                {
                    MethodInfo methodInfo = typeof(ReflectionHelper).GetMethod("FindAllTablesInternal", BindingFlags.Static | BindingFlags.NonPublic);
                    MethodInfo findTablesmethod = methodInfo.MakeGenericMethod(new Type[] { pInfo.PropertyType });
                    findTablesmethod.Invoke(null,  new object[]{schemaName,res,visitedTypes,pJoin,pLink,context});
                }
                //add property tables
                var table=TableName(typeof(T),schemaName,pInfo.Name);
                if(table!=null)
                    res.Add(new TableInfo { Name = table, ClassType = pInfo.PropertyType,JoinInfo=pJoin,LinkInfo=pLink });
            }
        }
        /// <summary>
        /// checks if the mapping should continue or stop from this property
        /// </summary>
        /// <param name="p"></param>
        /// <param name="schema"></param>
        /// <returns></returns>
        public static bool IsMappedAndInContext<T>(PropertyInfo p,string schema,ContextMemberCollection context=null){
            Type classType = typeof(T);
            //first check if this property is in context
            if (context != null && context.IsInContextCache(classType, p.Name, p.PropertyType))
                return true;            
            if (context!=null && !context.IsInContext(classType, p.Name,p.PropertyType))
                return false;            
            var tableName = TableName(classType, schema, p.Name);            
            var join = GetJoinInfo(classType, schema, p.Name);            
            var fieldNAme = FieldName(classType, schema, p.Name);
            var link = LinkTableName(classType, schema, p.Name);
            var classTableName = TableName(p.PropertyType, schema);
            var query = Query(p.PropertyType, schema);            
            var res=tableName != null || join != null || fieldNAme != null || classTableName != null || query!=null || link!=null;
            if (res && context!=null)
                context.Add2ContextCache(classType, p.Name);
            return res;

        }
        internal static Dictionary<string, string> GetFieldValuePairs(object container)
        {
            Dictionary<string, string> res = new Dictionary<string, string>();
            foreach (PropertyInfo pInfo in ReflectionCacheManager.GetProperties(container.GetType()))
            {
                string fName = FieldName(container.GetType(), "", pInfo.Name);
                if (!string.IsNullOrEmpty(fName))
                {
                    if (pInfo.PropertyType.IsEnum)
                    {
                        res.Add(fName, ToCode(pInfo.GetValue(container, null) as Enum));
                    }
                    else
                    {
                        res.Add(fName, pInfo.GetValue(container, null).ToString());
                    }
                }
            }
            return res;
        }
        internal static bool IsAutoLoad(PropertyInfo property,string schema)
        {
            var attrib = ReflectionCacheManager.GetCustomAttribute<ORMACAttribute>(schema,property.DeclaringType, property.Name);
            if (attrib != null)
                return attrib.AutoLoad;
            return true;
            
        }

        #region stored procedure
        /// <summary>
        /// gets the stored procedure information from <paramref name="methodName"/> of class <paramref name="repository"/>
        /// </summary>
        /// <param name="repository"></param>
        /// <param name="methodName"></param>
        /// <returns>stored procedure information</returns>
        internal static SPInfo GetSPInfo(object repository, string methodName)
        {
            SPInfo res=new SPInfo();
            res.Params = new List<ParameterInfo>();
            MethodInfo mInfo = repository.GetType().GetMethod(methodName);
            foreach(var attrib in mInfo.GetCustomAttributes(true)){
                if(attrib is StoredProcedureAttribute)
                    res.DBName=(attrib as StoredProcedureAttribute).Name;
                if(attrib is SPParamAttribute){
                    SPParamAttribute spAttrib=attrib as SPParamAttribute;
                    res.Params.Add(new ParameterInfo{
                        DBName=spAttrib.DBName,
                        IsOutput=spAttrib.IsOutput,
                        ParameterType=spAttrib.DBType
                    });
                }
            };
            return res;
        }
        #endregion
    }
}
